異步流程扯東扯西不知不覺竟然已經講了七天了!
實在是不好意思再拖更久,預計接下來三天一定要把後端的步驟 3 / 4 / 5 講完 (ง๑ •̀_•́)ง (ง๑ •̀_•́)ง (ง๑ •̀_•́)ง
程式碼請參考 D20/asynchronous-refactor
安裝時加上 -D,讓它不會被包裝進 production 裡面
npm i -D msw
以 Next.js 專案來說,直接在專案跟目錄執行以下指令,它就會把它依賴的 JS 檔案放到 public 路徑供你在執行階段取用
npx msw init public/ --save
前面有說到 MSW 可以同時適用於瀏覽器環境與伺服器環境。其中執行於瀏覽器的部分叫做 worker
、執行於伺服器環境的叫 server
。
server
這個名字好理解, worker
就比較特別了。其實 worker
背後跟 chorme 引擎的功能 service worker
有關,你可以想像 service worker
是一個運行於瀏覽器的 UI 執行緒之外的獨立執行緒,藉由 service worker
的幫助,我們就可以攔截前端程式發出的請求,並且把假的回應丟回去給 UI 執行緒。
rest.get(/* url */, (req,res,ctx)=>res(ctx.json({/* body */})))
msw 的 handler 語法很簡單
rest
代表 restful API,另外有支援 graphql
我們先不討論req
是請求物件,我們可以從中拿到 body / query / paramsres
代表送出回應ctx
決定回應內容為了方便使用,我們可以在建立 msw handler 時利用一些小技巧依照 endpoint 做整理,例如這樣
import { rest } from 'msw'
import { Resolver } from './utils'
const resolvers = {
'200 admin': (): Resolver => (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({ _tag: 'Administrator', name: req.params.username })
),
'200 admin once': (): Resolver => (req, res, ctx) =>
res.once(
ctx.status(200),
ctx.json({ _tag: 'Administrator', name: req.params.username })
),
'200 invalid': (): Resolver => (req, res, ctx) =>
res(ctx.status(200), ctx.json('lol')),
'404': (): Resolver => (req, res, ctx) => res(ctx.status(404)),
'404 once': (): Resolver => (req, res, ctx) => res.once(ctx.status(404)),
'500': (): Resolver => (req, res, ctx) => res(ctx.status(500)),
}
export const getUsersByNameHandler =
(baseUrl: string) => (key: keyof typeof resolvers) =>
rest.get(`${baseUrl}/api/v1/users/:username`, resolvers[key]())
使用時就會很方便、好讀
接下來介紹如何在 node.js 環境使用 MSW,以下範例不只是 vitest
,像是 jest
, mocha
等測試套件也都可以參考以下方法使用。
describe('UsersField.on AddUserEvent', () => {
const baseUrl = 'http://localhost'
const server = initServer(baseUrl)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
it('should ....'){
// all requests handled by msw
}
}
http://localhost
MSW 可以搭配 vitest 做 unitest 當然也就可以搭配 cypress 做 component test。
describe('<Users />', () => {
const worker = initWorker('')
before(() => {
worker.start()
})
beforeEach(() => {
cy.mount(<Users />)
})
afterEach(() => {
worker.resetHandlers()
})
after(() => {
worker.stop()
})
不論是 server 或是 worker,都有看到一個 resetHandler 的動作,這是甚麼意思呢? 難道說這些 handler 還可以中途變動? 沒錯,真的可以測到一半改 Handler ! 也正是因為怕影響到後續測試,所以我們才每次都做 resetHander !
it('should show 1 error message when user not found on 1 user selected', () => {
worker.use(getUsersByNameHandler('')('404'))
worker.use(getUsersByNameHandler('')('200 admin once'))
cy.get('input').type('richard_w{Enter}')
cy.get('.user-badge')
cy.get('input').type('richard{Enter}')
cy.get('.error-message').should('have.length', 1)
})
用以上範例來說明,我們第一個 worker.use
把原本都會回 200 替換成 404 的 mock API 替換成了都回 404,然後再補上一個會回一次 200 然後就失效的 handler,這樣一來我們第一個請求就會回 200,然後後面都回 404
it('should have not found error when status 404', async () => {
//arrange
server.use(getUsersByNameHandler(baseUrl)('404'))
const event = AddUserEvent.self
//act
const result = await pipe(
UsersField.on(event)(state),
Effect.merge,
Effect.runPromise
)
//assert
expect(result._tag).toBe('InvalidUsersField')
if (Option.isSome(result.error)) {
expect(result.error.value._tag).toBe('NotFoundError')
} else {
expect(result.error._tag).toBe('Some')
}
})
把場景轉換到 vitest 也是一樣,只是名字從 worker.use 換成 server.use,就這麼簡單 !